package utils

import (
	"github.com/gomodule/redigo/redis"
	"math/rand"
	"strconv"
)

const (
	// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
	lockCommand = `local value = ARGV[1]
local expire = ARGV[2]
if redis.call("GET", KEYS[1]) == value then
	redis.call("SET", KEYS[1], value, "PX", expire)
	return "OK"
else
	return redis.call("SET", KEYS[1], value, "NX", "PX", expire)
end
`
	delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
	return redis.call("DEL", KEYS[1])
else
	return 0
end
`

	letters         = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	millisPerSecond = 1000
	randomLen       = 16
)

// RedisLock represents redis lock object.
type RedisLock struct {
	redis redis.Conn
	key   string
	uuid  string
	ttl   uint64 // seconds
}

// NewRedisLock returns RedisLock object.
func NewRedisLock(redis redis.Conn, key string, ttl uint64) *RedisLock {
	return &RedisLock{
		redis: redis,
		key:   key,
		uuid:  randomStr(randomLen),
		ttl:   ttl,
	}
}

// Acquire acquire lock.
func (rl *RedisLock) Acquire() (bool, error) {
	resp, err := redis.NewScript(1, lockCommand).
		Do(rl.redis, rl.key, rl.uuid, strconv.Itoa(int(rl.ttl)*millisPerSecond))
	if err == redis.ErrNil {
		return false, nil
	} else if err != nil {
		return false, err
	} else if resp == nil {
		return false, nil
	}

	reply, ok := resp.(string)
	if ok && reply == "OK" {
		return true, nil
	}
	return false, nil
}

// Release release lock.
func (rl *RedisLock) Release() (bool, error) {
	resp, err := redis.NewScript(1, delCommand).Do(rl.redis, rl.key, rl.uuid)
	if err != nil {
		return false, err
	}

	reply, ok := resp.(int64)
	if !ok {
		return false, nil
	}

	return reply == 1, nil
}

func randomStr(n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return string(b)
}
